﻿/* Copyright 2010 Igor Francé
 * 
 * This file is part of ECMADoc (http://ecmadoc.codeplex.com).
 * 
 * ECMADoc is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * ECMADoc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with ECMADoc. 
 * If not, see http://www.gnu.org/licenses/.
 */
namespace EcmaDoc.Core.Configuration
{
	using System;
	using System.Collections.Generic;
	using System.Reflection;
	using System.Xml;
	using System.Xml.Schema;

	/// <summary>
	/// Defines the parser configuration.
	/// </summary>
	public class ParserConfig
	{
		/// <summary>
		/// Defines the xml namespace of EcmaDoc.
		/// </summary>
		public const string EcmaDocNamespace = @"http://www.cycle99.com/tools/ecmadoc";
		
		/// <summary>
		/// Defines the defalt file extension for the result documents generated by EcmaDoc.
		/// </summary>
		public const string DefaultFileExtension = @"html";

		/// <summary>
		/// Defines the name of the global namespace.
		/// </summary>
		public const string GlobalNamespace = "global";

		/// <summary>
		/// Defines the name of the default base type of JavaScript code items.
		/// </summary>
		public const string DefaultBaseType = "Object";
		
		/// <summary>
		/// Defines the name of the <c>void</c> type; the type used with cuntions that don't return a value.
		/// </summary>
		public const string DefaultVoidType = "void";

		/// <summary>
		/// Defines the name to use for anonymous functions.
		/// </summary>
		public const string AnonymousName = "anonymous";

		private const string Path = @"Resources\Parser.xml";
		private const string SchemaPath = @"Resources\Schemas\Parser.xsd";

		private static readonly object lockObj = new object();
		private static ParserConfig global;
		private static XmlNamespaceManager nsman;

		private Dictionary<string, TokenHandler> tokenHandlers;
		private Dictionary<string, MatchHandler> matchHandlers;

		private ParserConfig()
		{
			this.Tokens = new Dictionary<string, TokenInfo>();
			this.Expressions = new Dictionary<string, ExpressionInfo>();
			this.ParseOrder = new Dictionary<ParseMode, string[]>();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="ParserConfig"/> class.
		/// </summary>
		/// <param name="path">The path to the configuration document.</param>
		/// <exception cref="ArgumentNullException">If <paramref name="path"/> is empty or <c>null</c>.</exception>
		private ParserConfig(string path)
			: this()
		{
			if (string.IsNullOrEmpty(path))
				throw new ArgumentNullException("path");

			string configPath = Utilities.ExpandPath(path);
			string schemaPath = Utilities.ExpandPath(SchemaPath);

			XmlDocument config = new XmlDocument();
			config.Load(configPath);

			config.Schemas.Add(ParserConfig.EcmaDocNamespace, schemaPath);
			config.Validate(delegate(object sender, ValidationEventArgs e)
			{
				if (e.Exception != null)
				{
					throw new XmlSchemaValidationException(
						string.Format("Project schema validation {0}: {1}", e.Severity, e.Message), e.Exception);
				}
			});

			ParseXml(config.DocumentElement, this);
		}

		/// <summary>
		/// Gets the namespace manager that contains the namespaces in use by EcmaDoc..
		/// </summary>
		public static XmlNamespaceManager NamespaceManager
		{
			get
			{
				if (nsman == null)
				{
					nsman = new XmlNamespaceManager(new NameTable());
					nsman.AddNamespace("c", EcmaDocNamespace);
				}

				return nsman;
			}
		}

		/// <summary>
		/// Gets the global parser configuration instance.
		/// </summary>
		internal static ParserConfig Global
		{
			get
			{
				if (global == null)
				{
					lock (lockObj)
					{
						global = new ParserConfig(Path);
					}
				}

				return global;
			}
		}

		/// <summary>
		/// Gets the format string in use for generating constructor synopsis.
		/// </summary>
		internal string ConstructorSynopsis { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating function synopsis.
		/// </summary>
		internal string FunctionSynopsisFull { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating function synopsis.
		/// </summary>
		internal string FunctionSynopsisBase { get; private set; }

		/// <summary>
		/// Gets the format string  in use for generating return value synopsis.
		/// </summary>
		internal string ReturnValueFormat { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating argument synopsis.
		/// </summary>
		internal string ArgumentFormat { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating synopsis for non-global variables.
		/// </summary>
		internal string VariableSynopsis { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating synopsis for global variables.
		/// </summary>
		internal string VariableSynopsisGlobal { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating property synopsis.
		/// </summary>
		internal string PropertySynopsis { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating synopsis.
		/// </summary>
		internal string CommaFormat { get; private set; }

		/// <summary>
		/// Gets the format string in use for generating links.
		/// </summary>
		internal string LinkFormat { get; private set; }

		/// <summary>
		/// Gets the external types.
		/// </summary>
		internal ExternalTypeConfiguration ExternalTypes { get; private set; }

		internal Dictionary<string, TokenHandler> TokenHandlers
		{
			get
			{
				if (tokenHandlers == null)
				{
					lock (lockObj)
					{
						tokenHandlers = new Dictionary<string, TokenHandler>();
						MethodInfo[] methods = typeof(Handlers).GetMethods(
							BindingFlags.Public | 
							BindingFlags.NonPublic | 
							BindingFlags.Static);

						foreach (MethodInfo methodInfo in methods)
						{
							object[] attribs = methodInfo.GetCustomAttributes(typeof(TokenHandlerAttribute), true);
							if (attribs.Length != 0)
							{
								string name = ((TokenHandlerAttribute) attribs[0]).Name;
								TokenHandler handler = (TokenHandler) Delegate.CreateDelegate(typeof(TokenHandler), methodInfo);
								if (!tokenHandlers.ContainsKey(name))
									tokenHandlers.Add(name, handler);
							}
						}
					}
				}

				return tokenHandlers;
			}
		}

		internal Dictionary<string, MatchHandler> MatchHandlers
		{
			get
			{
				if (matchHandlers == null)
				{
					lock (lockObj)
					{
						matchHandlers = new Dictionary<string, MatchHandler>();
						MethodInfo[] methods = typeof(Handlers).GetMethods(
							BindingFlags.Public | 
							BindingFlags.NonPublic | 
							BindingFlags.Static);

						foreach (MethodInfo methodInfo in methods)
						{
							object[] attribs = methodInfo.GetCustomAttributes(typeof(MatchHandlerAttribute), true);
							if (attribs.Length != 0)
							{
								string name = ((MatchHandlerAttribute) attribs[0]).Name;
								MatchHandler handler = (MatchHandler) Delegate.CreateDelegate(typeof(MatchHandler), methodInfo);
								if (!matchHandlers.ContainsKey(name))
									matchHandlers.Add(name, handler);
							}
						}
					}
				}

				return matchHandlers;
			}
		}

		/// <summary>
		/// Gets the dictionary of supported tokens.
		/// </summary>
		internal Dictionary<string, TokenInfo> Tokens { get; private set; }

		/// <summary>
		/// Gets the dictionary of supported parse expressions.
		/// </summary>
		internal Dictionary<string, ExpressionInfo> Expressions { get; private set; }

		/// <summary>
		/// Gets the configured parse order.
		/// </summary>
		internal Dictionary<ParseMode, string[]> ParseOrder { get; private set; }

		internal static ParserConfig Create(XmlNode localParserConfig)
		{
			ParserConfig result = (ParserConfig) Global.MemberwiseClone();
			if (localParserConfig != null)
				ParseXml(localParserConfig, result);

			return result;
		}

		internal string MakeLink(string href, string text)
		{
			return string.Format(LinkFormat, href, text);
		}

		internal XmlDocument ToXml()
		{
			XmlDocument doc = new XmlDocument();
			XmlNode root = doc.AppendChild(doc.CreateElement("parser", EcmaDocNamespace));

			if (this.Tokens.Count != 0)
			{
				XmlNode tokens = root.AppendChild(doc.CreateElement("tokens", EcmaDocNamespace));
				foreach (TokenInfo info in Tokens.Values)
				{
					tokens.AppendChild(info.ToXml(doc));
				}
			}

			if (this.ParseOrder.Count != 0)
			{
				XmlNode modes = root.AppendChild(doc.CreateElement("modes", EcmaDocNamespace));
				foreach (ParseMode mode in ParseOrder.Keys)
				{
					XmlElement modeElem = (XmlElement) modes.AppendChild(doc.CreateElement("mode", EcmaDocNamespace));
					modeElem.SetAttribute("name", mode.ToString());
					modeElem.SetAttribute("value", ParseOrder[mode].Join(","));
				}
			}

			if (this.Expressions.Count != 0)
			{
				XmlNode expressions = root.AppendChild(doc.CreateElement("expressions", EcmaDocNamespace));
				foreach (ExpressionInfo info in Expressions.Values)
				{
					expressions.AppendChild(info.ToXml(doc));
				}
			}

			doc.AppendChild(doc.CreateElement("ConstructorSynopsis", EcmaDocNamespace)).InnerText = this.ConstructorSynopsis;
			doc.AppendChild(doc.CreateElement("VariableSynopsis", EcmaDocNamespace)).InnerText = this.VariableSynopsis;
			doc.AppendChild(doc.CreateElement("VariableSynopsisGlobal", EcmaDocNamespace)).InnerText = this.VariableSynopsisGlobal;
			doc.AppendChild(doc.CreateElement("PropertySynopsis", EcmaDocNamespace)).InnerText = this.PropertySynopsis;
			doc.AppendChild(doc.CreateElement("FunctionSynopsisFull", EcmaDocNamespace)).InnerText = this.FunctionSynopsisFull;
			doc.AppendChild(doc.CreateElement("FunctionSynopsisBase", EcmaDocNamespace)).InnerText = this.FunctionSynopsisBase;
			doc.AppendChild(doc.CreateElement("ReturnValueFormat", EcmaDocNamespace)).InnerText = this.ReturnValueFormat;
			doc.AppendChild(doc.CreateElement("ArgumentFormat", EcmaDocNamespace)).InnerText = this.ArgumentFormat;
			doc.AppendChild(doc.CreateElement("CommaFormat", EcmaDocNamespace)).InnerText = this.CommaFormat;
			doc.AppendChild(doc.CreateElement("LinkFormat", EcmaDocNamespace)).InnerText = this.LinkFormat;

			return doc;
		}

		private static void ParseXml(XmlNode configNode, ParserConfig target)
		{
			if (configNode == null)
				throw new ArgumentNullException("configNode");

			XmlNamespaceManager m = ParserConfig.NamespaceManager;
			XmlNode formatNode = configNode.SelectSingleNode("c:format", m);
			if (formatNode != null)
			{
				XmlNode valueNode;
				if ((valueNode = formatNode.SelectSingleNode("c:ConstructorSynopsis", m)) != null)
					target.ConstructorSynopsis = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:FunctionSynopsisFull", m)) != null)
					target.FunctionSynopsisFull = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:FunctionSynopsisBase", m)) != null)
					target.FunctionSynopsisBase = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:VariableSynopsis", m)) != null)
					target.VariableSynopsis = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:VariableSynopsisGlobal", m)) != null)
					target.VariableSynopsisGlobal = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:PropertySynopsis", m)) != null)
					target.PropertySynopsis = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:ReturnValueFormat", m)) != null)
					target.ReturnValueFormat = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:ArgumentFormat", m)) != null)
					target.ArgumentFormat = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:CommaFormat", m)) != null)
					target.CommaFormat = valueNode.InnerText;
				if ((valueNode = formatNode.SelectSingleNode("c:LinkFormat", m)) != null)
					target.LinkFormat = valueNode.InnerText;
			}

			XmlNodeList tokenNodes = configNode.SelectNodes("c:tokens/c:token", m);
			XmlNodeList exprNodes = configNode.SelectNodes("c:expressions/c:expression", m);
			XmlNodeList parseModes = configNode.SelectNodes("c:modes/c:mode", m);

			foreach (XmlElement tokenNode in tokenNodes)
			{
				string tokenName = tokenNode.GetAttribute("name");
				if (target.Tokens.ContainsKey(tokenName))
					target.Tokens[tokenName] = new TokenInfo(tokenNode);
				else
					target.Tokens.Add(tokenName, new TokenInfo(tokenNode));
			}

			foreach (XmlElement exprNode in exprNodes)
			{
				string exprName = exprNode.GetAttribute("name");
				if (target.Expressions.ContainsKey(exprName))
					target.Expressions[exprName] = new ExpressionInfo(exprNode);
				else
					target.Expressions.Add(exprName, new ExpressionInfo(exprNode));
			}

			foreach (XmlElement modeNode in parseModes)
			{
				ParseMode modeName = (ParseMode)
					Enum.Parse(typeof(ParseMode), modeNode.GetAttribute("name"), true);

				string[] modeOrder = modeNode.GetAttribute("order").Split(new[] { ',' },
					StringSplitOptions.RemoveEmptyEntries);

				for (int i = 0; i < modeOrder.Length; i++)
					modeOrder[i] = modeOrder[i].Trim();

				if (target.ParseOrder.ContainsKey(modeName))
					target.ParseOrder[modeName] = modeOrder;
				else
					target.ParseOrder.Add(modeName, modeOrder);
			}

			XmlElement typesNode = configNode.SelectSingleNode("c:types", nsman) as XmlElement;
			target.ExternalTypes = new ExternalTypeConfiguration(typesNode);
		}
	}
}
